package at.fhooe.mcm.logic.network;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import at.fhooe.mcm.logic.core.Core;
import at.fhooe.mcm.logic.event.EventManager;
import at.fhooe.mcm.logic.logging.ConsoleLoggingService;
import at.fhooe.mcm.logic.logging.LoggingService;
import at.fhooe.mcm.logic.network.events.PeerConnectedEvent;
import at.fhooe.mcm.logic.network.events.PeerDisconnectedEvent;

class PeerImpl implements Peer
{

	private static final int	PING_PKG			= 1;
	private static final int	CONNECT_PKG			= 2;
	private static final int	CONNECT_ACK_PGK		= 3;
	private static final int	DISCONNECT_PKG		= 4;
	private static final int	DISCONNECT_ACK_PKG	= 5;
	private static final int	CUSTOM_PGK			= 6;
	private static final long	MESSAGE_TIMEOUT		= 3000000000L;
	private static final long	PING_DELAY			= 500000000L;
	private static final long	CONNECTION_TIME_OUT	= 10000000000L;

	private SocketAddress		m_address;
	private boolean				m_connected;
	private EventManager		m_evtMngr;
	private LoggingService		m_logger;
	private Header				m_header;
	private long				m_lastSent;
	private long				m_lastReceived;
	private List<MessageInfo>	m_messageLog		= new ArrayList<MessageInfo>();
	private List<ByteBuffer>	m_messagesToSend	= new ArrayList<ByteBuffer>();
	private List<ByteBuffer>	m_receivedMessages	= new ArrayList<ByteBuffer>();

	public PeerImpl(Core _core, SocketAddress _address)
	{
		m_evtMngr = EventManager.getInstance(_core);
		m_logger = ConsoleLoggingService.getInstance(_core);
		m_address = _address;
		m_header = new Header();
	}

	public void putPackage(ByteBuffer _buffer)
	{
		// update time stamp
		m_lastReceived = System.nanoTime();

		// reader package header
		Header remoteHeader = new Header(_buffer);

		// acknowledge packages
		m_header.acknowledge(remoteHeader.getSequenceNumber());
		for (Iterator<MessageInfo> it = m_messageLog.iterator(); it.hasNext();)
		{
			if (remoteHeader.isAcknowledged(it.next().sequenceNumber))
			{
				it.remove();
			}
		}

		switch (remoteHeader.getPackageId())
		{

		case PING_PKG:
			// intentionally left empty
			break;

		case CONNECT_PKG:
			m_logger.logDebug(NetworkService.LOGGING_SOURCE, "received connect message");
			send(CONNECT_ACK_PGK, null);
			if (!isConnected())
			{
				send(CONNECT_PKG, null);
				m_logger.logDebug(NetworkService.LOGGING_SOURCE, "sending connect acknowledge message");
			}
			break;

		case CONNECT_ACK_PGK:
			m_logger.logDebug(NetworkService.LOGGING_SOURCE, "received connect acknowledge message");
			m_connected = true;
			m_evtMngr.enqueueEvent(new PeerConnectedEvent(this));
			break;

		case DISCONNECT_PKG:
			m_logger.logDebug(NetworkService.LOGGING_SOURCE, "received disconnect message");
			if (isConnected())
			{
				send(DISCONNECT_PKG, null);
				m_logger.logDebug(NetworkService.LOGGING_SOURCE, "sending disconnect acknowledge message");
			}
			send(DISCONNECT_ACK_PKG, null);
			break;

		case DISCONNECT_ACK_PKG:
			m_logger.logDebug(NetworkService.LOGGING_SOURCE, "received disconnect acknowledge message");
			m_connected = false;
			m_evtMngr.enqueueEvent(new PeerDisconnectedEvent(this));
			break;

		case CUSTOM_PGK:
			ByteBuffer message = ByteBuffer.allocate(_buffer.remaining());
			message.put(_buffer);
			message.flip();
			m_receivedMessages.add(message);
			break;
		}

	}

	public void update(DatagramChannel _channel) throws IOException
	{
		long time = System.nanoTime();

		// send queued messages
		for (ByteBuffer message : m_messagesToSend)
		{
			// TODO find a better way of doing this
			while (_channel.send(message, m_address) == 0)
				;
		}
		m_messagesToSend.clear();

		// check for unacknowledged messages
		for (Iterator<MessageInfo> it = m_messageLog.iterator(); it.hasNext();)
		{
			MessageInfo info = it.next();
			if (time - info.timeStamp > MESSAGE_TIMEOUT)
			{
				it.remove();
				m_logger.logInfo(NetworkService.LOGGING_SOURCE, "package lost (" + info.sequenceNumber + ")");
			}
		}

		if (!isConnected())
		{
			return;
		}

		// send ping message if necessary
		if (time - m_lastSent > PING_DELAY)
		{
			send(PING_PKG, null);
		}

		// is remote peer still alive?
		if (time - m_lastReceived > CONNECTION_TIME_OUT)
		{
			m_connected = false;
			m_messageLog.clear();
			m_evtMngr.enqueueEvent(new PeerDisconnectedEvent(this));
		}
	}

	@Override
	public void connect()
	{
		send(CONNECT_PKG, null);
	}

	@Override
	public void disconnect()
	{
		send(DISCONNECT_PKG, null);
	}

	@Override
	public boolean isConnected()
	{
		return m_connected;
	}

	@Override
	public ByteBuffer receive()
	{
		if (m_receivedMessages.isEmpty())
		{
			return null;
		}

		ByteBuffer buffer = ByteBuffer.allocate(m_receivedMessages.get(0).limit());

		buffer.put(m_receivedMessages.remove(0));
		return buffer;
	}

	@Override
	public void send(ByteBuffer buffer)
	{
		send(CUSTOM_PGK, buffer);
	}

	@Override
	public SocketAddress getAddress()
	{
		return m_address;
	}

	private void send(int _id, ByteBuffer _buffer)
	{
		m_header.incSequenceNumber();
		m_header.setPackageId(_id);

		int size = _buffer != null ? _buffer.remaining() + Header.SERIALIZED_SIZE : Header.SERIALIZED_SIZE;
		ByteBuffer message = ByteBuffer.allocate(size);
		m_header.serialize(message);
		if (_buffer != null)
		{
			message.put(_buffer);
		}
		message.flip();

		m_messagesToSend.add(message);
		m_lastSent = System.nanoTime();
		m_messageLog.add(new MessageInfo(m_header.getSequenceNumber(), m_lastSent));
	}

	private static class MessageInfo
	{

		private int		sequenceNumber;
		private long	timeStamp;

		public MessageInfo(int _seq, long _time)
		{
			sequenceNumber = _seq;
			timeStamp = _time;
		}
	}

}
